#!/usr/bin/perl
#
# xref-quadlet-docs - cross-validate quadlet man page vs actual source
#
# $Id: .perl-template,v 1.2 2020/03/03 20:08:31 esm Exp esm $
#
package Podman::CrossrefQuadletDocs;

use v5.14;
use utf8;

use strict;
use warnings;

(our $ME = $0) =~ s|.*/||;
our $VERSION = '0.1';

###############################################################################
# BEGIN user-customizable section

our $Go  = 'pkg/systemd/quadlet/quadlet.go';
our $Doc = 'docs/source/markdown/podman-systemd.unit.5.md';

# END   user-customizable section
###############################################################################

###############################################################################
# BEGIN boilerplate args checking, usage messages

sub usage {
    print  <<"END_USAGE";
Usage: $ME [OPTIONS]

$ME cross-checks quadlet documentation between the Go source[Go]
and the man page[MD].

 [Go]: $Go
 [MD]: $Doc

We check that:

  * all keys in [Go] are documented in [MD]
  * all keys in [MD] exist in [Go]
    * any keys listed in [MD] tables also have a description block
      and vice-versa
  * all keys everywhere are in sorted order

OPTIONS:

  --help         display this message
  --version      display program name and version
END_USAGE

    exit;
}

# Command-line options.  Note that this operates directly on @ARGV !
our $debug   = 0;
sub handle_opts {
    use Getopt::Long;
    GetOptions(
        'debug!'     => \$debug,

        help         => \&usage,
        man          => \&man,
        version      => sub { print "$ME version $VERSION\n"; exit 0 },
    ) or die "Try `$ME --help' for help\n";
}

# END   boilerplate args checking, usage messages
###############################################################################

############################## CODE BEGINS HERE ###############################

# The term is "modulino".
__PACKAGE__->main()                                     unless caller();

# Main code.
sub main {
    # Note that we operate directly on @ARGV, not on function parameters.
    # This is deliberate: it's because Getopt::Long only operates on @ARGV
    # and there's no clean way to make it use @_.
    handle_opts();                      # will set package globals

    # No command-line args
    die "$ME: Too many arguments; try $ME --help\n"                 if @ARGV;

    my $errs = 0;
    $SIG{__WARN__} = sub {
        print STDERR "@_";
        ++$errs;
    };

    # Assume that Go source file has Truth
    my $true_keys = read_go($Go);

    # Read md file, compare against Truth
    crossref_doc($Doc, $true_keys);

    exit $errs;
}


#############
#  read_go  #  Returns list of key strings found in quadlet.go
#############
sub read_go {
    my $path = shift;
    open my $fh, '<', $path
        or die "$ME: Cannot read $path: $!\n";;

    my @found;                          # List of key strings
    my $last_constname;                 # Most recently seen const name

    while (my $line = <$fh>) {
        # Only interested in lines of the form   KeyFoo = "Foo"
        if ($line =~ /^\s+Key(\S+)\s+=\s+"(\S+)"/) {
            my ($constname, $keystring) = ($1, $2);

            my $deprecated = ($line =~ m!\s//\s+deprecated!i);

            # const name must be the same as the string
            $constname eq $keystring
                or warn "$ME: $path:$.: mismatched strings: Key$constname = \"$keystring\"\n";

            # Sorting check.
            if ($last_constname) {
                if (lc($constname) lt lc($last_constname)) {
                    warn "$ME: $path:$.: out-of-order variable name 'Key$constname' should precede 'Key$last_constname'\n";
                }
            }
            $last_constname = $constname;

            push @found, $keystring
                unless $deprecated;
        }
    }
    close $fh;

    \@found;
}

##################
#  crossref_doc  #  Read the markdown page, cross-check against Truth
##################
sub crossref_doc {
    my $path      = shift;              # in: path to .md file
    my $true_keys = shift;              # in: AREF, list of keys from .go

    open my $fh, '<', $path
        or die "$ME: Cannot read $path: $!\n";;

    my $unit = '';
    my %documented;
    my @found_in_table;
    my @described;

    # Helper function: when done reading description blocks,
    # make sure that there's one block for each key listed
    # in the table. Defined as a local function because we
    # need to call it from two different places.
    my $crossref_against_table = sub {
        for my $k (@found_in_table) {
            grep { $_ eq $k } @described
                or warn "$ME: key not documented: '$k' listed in table for unit '$unit' but not actually documented\n";
        }
    };

    # Main loop: read the docs line by line
    while (my $line = <$fh>) {
        chomp $line;

        # New section, with its own '| table |' and '### Keyword blocks'
        if ($line =~ /^##\s+(\S+)\s+(?:units|section)\s+\[(\S+)\]/) {
            my $new_unit = $1;
            $new_unit eq $2
                or warn "$ME: $path:$.: inconsistent block names in '$line'\n";

            $crossref_against_table->();

            $unit = $new_unit;

            # Reset, because each section has its own table & blocks
            @found_in_table = ();
            @described = ();
            next;
        }

        # Table line
        if ($line =~ s/^\|\s+//) {
            next if $line =~ /^\*\*/;           # title
            next if $line =~ /^-----/;          # divider

            if ($line =~ /^([A-Z][A-Za-z6]+)=/) {
                my $key = $1;

                grep { $_ eq $key } @$true_keys
                    or warn "$ME: $path:$.: unknown key '$key' (not present in $Go)\n";

                # Sorting check
                if (@found_in_table) {
                    if (lc($key) lt lc($found_in_table[-1])) {
                        warn "$ME: $path:$.: out-of-order key '$key' in table\n";
                    }
                }

                push @found_in_table, $key;
                $documented{$key}++;
            }
            else {
                warn "$ME: $path:$.: cannot grok table line '$line'\n";
            }
        }

        # Description block
        elsif ($line =~ /^###\s+`(\S+)=`/) {
            my $key = $1;

            # Check for dups and for out-of-order
            if (@described) {
                if (lc($key) lt lc($described[-1])) {
                    warn "$ME: $path:$.: out-of-order key '$key'\n";
                }
                if (grep { lc($_) eq lc($key) } @described) {
                    warn "$ME: $path:$.: duplicate key '$key'\n";
                }
            }

            grep { $_ eq $key } @found_in_table
                or warn "$ME: $path:$.: key '$key' is not listed in table for unit/section '$unit'\n";

            push @described, $key;
            $documented{$key}++;
        }
    }

    close $fh;

    # Final cross-check between table and description blocks
    $crossref_against_table->();

    # Check that no Go keys are missing

    (my $md_basename = $path) =~ s|^.*/||;
    for my $k (@$true_keys) {
        $documented{$k}
            or warn "$ME: undocumented key: '$k' not found anywhere in $md_basename\n";
    }
}

1;
